home *** CD-ROM | disk | FTP | other *** search
/ Nebula 2 / Nebula Two.iso / SourceCode / GameKit / gamekit-1 / HighScoreServer.m < prev    next >
Text File  |  1995-06-12  |  17KB  |  518 lines

  1. // HighScoreServer.m
  2. // under development; anything with ***** is yet to be completed
  3.  
  4. // This class is the actual high score server for any given game.
  5. // You shouldn't need to subclass it:  Just change the class of
  6. // HighScoreSlot used and change the GameInfo object's parameters.
  7.  
  8. // I need to do exception handling in here to catch any DO problems,
  9. // but haven't got around to it yet, so exceptions will currently crash
  10. // the server. :-<  *****
  11.  
  12. // Note that a game with multiple tables simply uses multiple servers,
  13. // each with a different name.  (For example:  NX_Invaders.easy,
  14. // NX_Invaders.hard would be a possibility.  The HighScoreController in
  15. // the game itself handles all the coordination between tables; by
  16. // default they are numbered.)
  17.  
  18. // In the future it would be nice to allow the game to send a .o file
  19. // over the connection for any high score slots the server can't deal
  20. // with.  This would allow the server to truly dynamically update itself!
  21. // Right now, though, there's not an easy way to do this and I don't have
  22. // the time to take care of it, since other parts of the GameKit need more
  23. // attention.  The method frameworks are in the protocol, though.
  24.  
  25. #import <appkit/appkit.h>
  26. #import <daymisckit/daymisckit.h>
  27. #import <gamekit/gamekit.h>
  28. #import <remote/NXProxy.h>
  29. #import <objc/objc-runtime.h>
  30. #import <objc/objc-load.h>
  31.  
  32. // this is where the files are stored.  Each file is named
  33. // using the convention "<game's name>.highscores"  The files
  34. // themselves are all typed streams with archived GameInfo and
  35. // HighScoreTable objects in them.  Override the -pathToTables
  36. // method to change this.
  37. #define PATH_TO_TABLES "/usr/local/games/highscores/"
  38.  
  39. static BOOL loggingIsOn;    // a flag to turn all logging on or
  40.     // off for all servers in an application
  41. static id logFile;
  42. //  define DEBUGLOG for even more logging...
  43.  
  44. @implementation HighScoreServer
  45.  
  46. + turnLoggingOn:(BOOL)flag
  47. {
  48.     loggingIsOn = flag;
  49.     if (loggingIsOn) { // make new log file when turning on logging
  50.         // (This allows us to change the name of the file by toggling
  51.         // the on and off.  A subclass would have to implement +makeLogFile
  52.         // in order to actually do something like this.)
  53.         if (logFile) [logFile free];
  54.         logFile = [self makeLogFile];
  55.     }
  56.     return self;
  57. }
  58.  
  59. + makeLogFile    // override to change where loggin goes to.
  60. {                // Just return a DAYLogFile instance!
  61.     id fileName = [[DAYString newWithString:PATH_TO_TABLES] cat:"log"];
  62.     // I don't use an attendant lock file since there should only
  63.     // ever be one server after the file -- note that I'm intending
  64.     // this facility for remote servers, not local.  If you are going
  65.     // to do logging from a local game, you _better_ give it a lock file!!!
  66.     id newLog = [[[DAYLogFile alloc] init] setFileName:fileName];
  67.     return newLog;
  68. }
  69.  
  70. - init    { return [self error:"Use -initForGame: (not -init)"]; }
  71. - initForGame:(const char *)name // designated initializer
  72. {
  73.     [super init];
  74.     if (!name) { // must give us a name or we'll barf.
  75. #ifdef DEBUGLOG
  76.         if (loggingIsOn) {
  77.             id tempString = [DAYString
  78.                 newWithString:"An attempt was made to start a NULL server."];
  79.             [logFile addLineToLogFile:tempString];
  80.             [tempString free];
  81.         }
  82. #endif
  83.         fprintf(stderr, "HighScoreServer error:  can't init for null game.\n");
  84.         [self free];  
  85.         return nil; // note that nothing is returned...
  86.     }
  87.     // find file that holds the high score file
  88.     // if non-existent, create an empty table, otherwise we load it in
  89.     gameName = [[DAYString alloc] initString:name];
  90.     if (loggingIsOn) {
  91.         id tempString = [[gameName copy] cat:":  Starting server.\n"];
  92.         [logFile addLineToLogFile:tempString];
  93.         [tempString free];
  94.     }
  95.     clientList = [[List alloc] initCount:0];
  96.     connList = [[List alloc] initCount:0];
  97.     clientAuth = [[Storage alloc]
  98.             initCount:0 elementSize:sizeof(char) description:"c"];
  99.     [[[GameInfo alloc] init] free];    // forces class to be linked into serverd
  100.     // without the need for the ld flag that links in the whole library...
  101.     gameInfo = nil;    // we'll force the first client to check in to send it    
  102.     // build the name of the file where we store the highscores
  103.     scoreFile = [[DAYString alloc] initString:[self pathToTables]];
  104.     [scoreFile cat:"/"];
  105.     [scoreFile concatenate:gameName];
  106.     [scoreFile cat:".highscores"];
  107.     [self load];    // load in a highscore table
  108.     return self;
  109. }
  110.  
  111. - (const char *)pathToTables
  112. {    // subclass can override this to customize.
  113.     return PATH_TO_TABLES;
  114. }
  115.  
  116. - (oneway)addSlotCode:(bycopy in id)code    // not yet implemented
  117. { // *****
  118.     if (loggingIsOn) {
  119.         id tempString = [[gameName copy]
  120.             cat:":  Slot code sent to server.\n"];
  121.         [logFile addLineToLogFile:tempString];
  122.         [tempString free];
  123.     }
  124.     return self;
  125. }
  126.  
  127. - (oneway)setGameInfo:(bycopy in id)info
  128. {
  129.     id emptySlotClass;
  130.     const char *slotClass = [[info slotType] stringValue];
  131.     
  132.     // avoid freeing GameInfo if in a game and not a remote server
  133.     if (![NXApp delegate]) if (gameInfo) [gameInfo free];
  134.     gameInfo = info;    // we now have a gameInfo object that tells us
  135.     // how to do things.  Without this, we can only function according
  136.     // to the gamekit defaults.
  137.     emptySlotClass = objc_lookUpClass(slotClass);
  138.     if (!emptySlotClass) {    // can't find the class, so try and load it
  139.         long ret;
  140.         char *fileName = (char *)malloc(strlen(slotClass) +
  141.                 strlen([self pathToTables]) + 3);
  142.         char *fileNames[2] = {fileName, NULL};
  143.         sprintf(fileName, "%s%s.o", [self pathToTables], slotClass);
  144.         ret = objc_loadModules(fileNames, NULL, NULL, NULL, NULL);
  145.         emptySlotClass = objc_lookUpClass(slotClass);
  146.         if (ret || !emptySlotClass) {
  147.             // couldn't load the class so tell the clients we can't help.
  148.             [clientList makeObjectsPerform:@selector(cantBeServed:)
  149.                     with:gameName];
  150.             if (![NXApp delegate]) [gameInfo free];
  151.             gameInfo = nil; // assume we're still uninitted.
  152.             return self;
  153.         }
  154.     }
  155.     [table setEmptySlotClass:emptySlotClass];
  156.     [table setMaxHighScores:[gameInfo maxHighScores]];
  157.     // ***** need to figure out which table we are so that this works right
  158.     [table setMaxScoresPerPlayer:[gameInfo maxScoresPerPlayerTable:0 net:YES]];
  159. #ifdef NOISYDEBUG
  160.     fprintf(stderr, "HighScoreServer:  maxScorePerPlayer is %d.\n",
  161.             [gameInfo maxScoresPerPlayerTable:0 net:YES]);
  162. #endif
  163.     if (loggingIsOn) {
  164.         id tempString = [[gameName copy] cat:":  Got GameInfo object.\n"];
  165.         [logFile addLineToLogFile:tempString];
  166.         [gameInfo dumpToLog:logFile];
  167.         [tempString free];
  168.     }
  169.     [self save]; // so even if no slot is sent we at least have GameInfo saved.
  170.     return self;
  171. }
  172.  
  173. - (oneway)setTemplate:(bycopy in id)newTemplate
  174. {
  175.     if (template) [template free];
  176.     template = newTemplate;
  177.     if (haveNonTemplateTable) {
  178.         int i;
  179.         haveNonTemplateTable = NO;
  180.         [self _makeTableRatherThanLoad];
  181.         for (i=0; i<[clientList count]; i++)
  182.             [[clientList objectAt:i] acceptTable:table name:gameName];
  183.     }
  184.     if (loggingIsOn) {
  185.         id tempString = [[gameName copy]
  186.                 cat:":  Got table template object.\n"];
  187.         [logFile addLineToLogFile:tempString];
  188.         [tempString free];
  189.     }
  190.     return self;
  191. }
  192.  
  193. - free
  194. {
  195.     // free our private strings
  196.     [scoreFile free];
  197.     [gameName free];
  198.     // free all high score slots and tables
  199.     [[table freeObjects] free];
  200.     // free other items (internal params)
  201.     [clientList free];
  202.     [clientAuth free];
  203.     [connList free];
  204.     if (![NXApp delegate]) [gameInfo free];
  205.     return [super free];
  206. }
  207.  
  208. // methods that the client can call
  209. - (oneway)clientDying:(in id <HighScoreClient>)client
  210.         // alerts server that a client is going away
  211. {
  212.     // remove client from the list
  213.     unsigned num = [clientList indexOf:client];
  214.     while (num != NX_NOT_IN_LIST) {
  215.         [clientList removeObject:client];
  216.         [connList removeObjectAt:num];
  217.         [clientAuth removeElementAt:num];
  218.         num = [clientList indexOf:client];
  219.     }
  220. #ifdef DEBUGLOG
  221.     if (loggingIsOn) {
  222.         id tempString = [[gameName copy] cat:":  A client left.\n"];
  223.         [logFile addLineToLogFile:tempString];
  224.         [gameInfo dumpToLog:logFile];
  225.         [tempString free];
  226.     }
  227. #endif
  228.     return self;
  229. }
  230.  
  231. - senderIsInvalid:sender
  232. {
  233.     int i;
  234.     BOOL changed = YES;
  235.     while (changed) {
  236.         changed = NO;
  237.         for (i=0; i<[connList count]; i++) {
  238.             if (sender == [connList objectAt:i]) {
  239.                 [clientList removeObjectAt:i];
  240.                 [connList removeObjectAt:i];
  241.                 [clientAuth removeElementAt:i];
  242.                 changed = YES;
  243.                 break; // for loop -- have to restart iteration, since
  244.                 // list object has been changed now, but we also
  245.                 // want to be sure that we remove multiple pointers
  246.                 // to dead clients if they exist, hence the while loop
  247.     }    }    }
  248. #ifdef DEBUGLOG
  249.     if (loggingIsOn) {
  250.         id tempString = [[gameName copy] cat:":  A client died.\n"];
  251.         [logFile addLineToLogFile:tempString];
  252.         [gameInfo dumpToLog:logFile];
  253.         [tempString free];
  254.     }
  255. #endif
  256.     return self;
  257. }
  258.  
  259. - (oneway)clientCheckIn:(in id <HighScoreClient>)client
  260.         // new client alerts of presence so that server
  261.         // can notify client of changes in the table
  262. {
  263.     BOOL *auth = (BOOL *)malloc(sizeof(BOOL));
  264.     NXConnection *conn;
  265.     *auth = NO;
  266.     if ([(NXProxy *)client isProxy]) // could be Object subclass, too.
  267.             conn = [(NXProxy *)client connectionForProxy];
  268.     else {    // client is local, so no connection, and it's authorized.
  269.         conn = nil;
  270.         *auth = YES;
  271.     }
  272.     // add the client to the list
  273.     [clientList addObject:client];
  274.     [connList addObject:conn];
  275.     [clientAuth addElement:auth];
  276.     [conn registerForInvalidationNotification:self];
  277. #ifdef DEBUGLOG
  278.     if (loggingIsOn) {
  279.         id tempString = [[gameName copy] cat:":  A client checked in.\n"];
  280.         [logFile addLineToLogFile:tempString];
  281.         [gameInfo dumpToLog:logFile];
  282.         [tempString free];
  283.     }
  284. #endif
  285.     // ask for gameInfo object if we don't have it yet
  286.     if (!gameInfo) [client sendGameInfoTo:gameName];
  287.     else [client acceptTable:table name:gameName];
  288.     return self;
  289. }
  290.                     
  291. - (oneway)addSlot:newSlot            // new high scores come in here
  292.         fromClient:(in id <HighScoreClient>)client    // and go to all clients
  293. {
  294.     int c;
  295.     id <HighScoreClient> tempClient; // gets rid of protocol warnings
  296.     
  297.     if (loggingIsOn) {
  298.         id tempString = [[gameName copy] cat:":  A new slot was submitted.\n"];
  299.         [logFile addLineToLogFile:tempString];
  300.         [newSlot dumpToLog:logFile];
  301.         [tempString free];
  302.     }
  303.     // insert the new score into the table; return if it doesn't fit
  304.     if (![table addSlot:newSlot]) return self;
  305.     // The server will send each client -addSlot:tableName: messages for every
  306.     // slot which changes while the client is connected.  That way, the client
  307.     // can update panels, etc. when someone else gets a new highscore.
  308.     for (c=0; c<[clientList count]; c++) {
  309.         tempClient = [clientList objectAt:c];
  310.         if (client != tempClient) {
  311.             [tempClient addSlot:newSlot tableName:gameName];
  312.         }
  313.     }
  314.     // save the table to a file.  Done every time there's a change
  315.     // so that nothing is lost if we crash, die, or get killed.
  316.     [self save];
  317.     return self;
  318. }
  319.  
  320. - (BOOL)authorize:(id <HighScoreClient>)client
  321. {    // get password from client and compare to gameInfo password via crypt()
  322.     id password = [client password];    // returns a DAYString
  323.     id encr = [password encrypt:[[gameInfo encryptedPassword] left:2]];
  324.     BOOL ret = YES;
  325.     if ([encr compareTo:[gameInfo encryptedPassword]]) ret = NO;
  326.     if (loggingIsOn) {
  327.         id tempString = [[gameName copy] cat:":  Client sent password "];
  328.         if (ret) [tempString cat:"successfully.\n"];
  329.         else [tempString cat:"but it was wrong.\n"];
  330.         [logFile addLineToLogFile:tempString];
  331.         [tempString free];
  332.     }
  333.     return ret;
  334. }
  335.  
  336. - (BOOL)validateClient:(id <HighScoreClient>)client
  337. {
  338.     unsigned num = [clientList indexOf:client];
  339.     BOOL *valid = [clientAuth elementAt:num];
  340.     if (*valid != YES) return [self authorize:client];
  341.     return YES;
  342. }
  343.  
  344. - (oneway)clearTable:(in id <HighScoreClient>)sender
  345.                         // zero out the table.  Asks sender for
  346.                         // proper authentication first.  See the table
  347.                         // editing app for an example of how to do this.
  348. { // need to validate sender first off
  349.     int c; id <HighScoreClient> tempClient; // gets rid of protocol warnings
  350.     if (![self validateClient:sender]) return self;
  351.     if (loggingIsOn) {
  352.         id tempString = [[gameName copy] cat:":  Client cleared the table.\n"];
  353.         [logFile addLineToLogFile:tempString];
  354.         [tempString free];
  355.     }
  356.     // clear the table
  357.     [table freeObjects];
  358.     if (template) { // if available, use the empty "template"
  359.         [table free];
  360.         table = [template copy];
  361.     }
  362.     [self save];
  363.     // now send it off to the clients
  364.     for (c=0; c<[clientList count]; c++) {
  365.         tempClient = [clientList objectAt:c];
  366.         // ***** commented out since we want to update _all_ clients!
  367.         //if (sender != tempClient) {
  368.             [tempClient acceptTable:table name:gameName];
  369.         //}
  370.     }
  371.     return self;
  372. }
  373.  
  374. - (oneway)deleteSlot:(in int)i client:(in id <HighScoreClient>)sender
  375. {
  376.     id <HighScoreClient> tempClient; // gets rid of protocol warnings
  377.     int c;
  378.     if (![self validateClient:sender]) return self;
  379.     if (loggingIsOn) {
  380.         char *string = (char *)malloc(16);
  381.         id tempString = [[gameName copy] cat:":  Client removed slot #"];
  382.         sprintf(string, "%d\n", i);
  383.         [tempString cat:string];
  384.         [logFile addLineToLogFile:tempString];
  385.         [tempString free];
  386.         free(string);
  387.     }
  388.     [table removeObjectAt:i];
  389.     for (c=0; c<[clientList count]; c++) {
  390.         tempClient = [clientList objectAt:c];
  391.         if (sender != [clientList objectAt:c]) {
  392.             [tempClient removeSlotAt:i tableName:gameName];
  393.         }
  394.     }
  395.     [self save];
  396.     return self;
  397. }
  398.  
  399. - (oneway)replaceSlot:(in int)i with:(bycopy in id)aSlot
  400.         client:(in id <HighScoreClient>)sender
  401. {
  402.     int c; id <HighScoreClient> tempClient; // gets rid of protocol warnings
  403.     if (![self validateClient:sender]) return self;
  404.     if (loggingIsOn) {
  405.         char *string = (char *)malloc(16);
  406.         id tempString = [[gameName copy] cat:":  Client replaced slot #"];
  407.         sprintf(string, "%d\n", i);
  408.         [tempString cat:string];
  409.         [logFile addLineToLogFile:tempString];
  410.         [tempString free];
  411.         free(string);
  412.     }
  413.     [table replaceObjectAt:i with:aSlot];
  414.     for (c=0; c<[clientList count]; c++) {
  415.         tempClient = [clientList objectAt:c];
  416.         if (sender != tempClient) {
  417.             [tempClient replaceSlotAt:i with:aSlot tableName:gameName];
  418.         }
  419.     }
  420.     [self save];
  421.     return self;
  422. }
  423.  
  424. // ***** not yet implemented.  Will be used mostly by editors to guarantee
  425. // edits properly sent to all clients.  For locking all other clients out
  426. // of a table temporarily...
  427. - (oneway)lockTable { return self; }
  428. - (oneway)unlockTable { return self; }
  429.  
  430. - (const char *)gameName    // Name of the game we are serving
  431. { return [gameName stringValue]; }
  432.  
  433. - save        // flushes the table to the appropriate file.
  434. {
  435.     NXTypedStream *typedStream;
  436.     haveNonTemplateTable = NO;
  437.     NX_DURING    
  438.     typedStream = NXOpenTypedStreamForFile([scoreFile stringValue],
  439.             NX_WRITEONLY);
  440.     NXWriteObject(typedStream, gameInfo);
  441.     NXWriteObject(typedStream, table);
  442.     NXWriteObject(typedStream, template);
  443.     NXCloseTypedStream(typedStream);
  444.     NX_HANDLER
  445.         // deal with typed stream errors here *****
  446.         fprintf(stderr, "Exception %d raised in -save.\n",    
  447.                 NXLocalHandler.code);
  448.     NX_ENDHANDLER
  449. #ifdef DEBUGLOG
  450.     if (loggingIsOn) {
  451.         id tempString = [[gameName copy] cat:":  Saved table.\n"];
  452.         [logFile addLineToLogFile:tempString];
  453.         [tempString free];
  454.     }
  455. #endif
  456.     return self;
  457. }
  458.  
  459. - _makeTableRatherThanLoad
  460. {
  461.     table = [template copy];
  462.     if (!table) {
  463.         haveNonTemplateTable = YES;
  464.         table = [[HighScoreTable alloc] init]; // ***** should follow gameInfo params...
  465.     }
  466. #ifdef DEBUGLOG
  467.     if (loggingIsOn) {
  468.         id tempString = [[gameName copy]
  469.                 cat:":  Built table from template.\n"];
  470.         [logFile addLineToLogFile:tempString];
  471.         [tempString free];
  472.     }
  473. #endif
  474.     return self;
  475. }
  476.  
  477. - load        // load the table from a file, if it exists.
  478. {
  479.     NXTypedStream *typedStream;
  480.     FILE *testFile;
  481.         
  482.     haveNonTemplateTable = NO;
  483.     // for some reason, NXOpenTypedStreamForFile() isn't returning
  484.     // NULL for me when the file doesn't exist, so I check for the
  485.     // file's existence first.
  486.     testFile = fopen([scoreFile stringValue], "r");
  487.     if (!testFile) return [self _makeTableRatherThanLoad];
  488.     fclose(testFile);
  489.     NX_DURING
  490.     typedStream = NXOpenTypedStreamForFile([scoreFile stringValue],
  491.             NX_READONLY);
  492.     if (!typedStream) return [self _makeTableRatherThanLoad];
  493.     
  494.     if (table) [[table freeObjects] free];
  495.     if (template) [[template freeObjects] free];
  496.     gameInfo = NXReadObject(typedStream);
  497.     // ***** should load dynamic classes here; GameInfo might require it!
  498.     table = NXReadObject(typedStream);
  499.     template = NXReadObject(typedStream);
  500.     NXCloseTypedStream(typedStream);
  501.     NX_HANDLER
  502.         // deal with typed stream errors here *****
  503.         [self _makeTableRatherThanLoad];
  504.         fprintf(stderr, "Exception %d raised in -load.\n",    
  505.                 NXLocalHandler.code);
  506.     NX_ENDHANDLER
  507. #ifdef DEBUGLOG
  508.     if (loggingIsOn) {
  509.         id tempString = [[gameName copy] cat:":  Loaded table.\n"];
  510.         [logFile addLineToLogFile:tempString];
  511.         [tempString free];
  512.     }
  513. #endif
  514.     return self;
  515. }
  516.  
  517.  
  518. @end